﻿//------------------------------------------------------------------------------
//  此代码版权声明为全文件覆盖，如有原作者特别声明，会在下方手动补充
//  此代码版权（除特别声明外的代码）归作者本人Diego所有
//  源代码使用协议遵循本仓库的开源协议及附加协议
//  Gitee源代码仓库：https://gitee.com/diego2098/ThingsGateway
//  Github源代码仓库：https://github.com/kimdiego2098/ThingsGateway
//  使用文档：https://thingsgateway.cn/
//  QQ群：605534569
//------------------------------------------------------------------------------

using System.Runtime.InteropServices;
using System.Security.Cryptography.X509Certificates;

using SecureString = System.Security.SecureString;

namespace ThingsGateway.NewLife.X.Security;

#pragma warning disable CS8625 // 无法将 null 字面量转换为非 null 的引用类型。

/// <summary>证书</summary>
/// <remarks>http://blogs.msdn.com/b/dcook/archive/2008/11/25/creating-a-self-signed-certificate-in-c.aspx</remarks>
public class Certificate
{
    /// <summary>建立自签名证书</summary>
    /// <param name="x500"></param>
    /// <returns></returns>
    public static Byte[] CreateSelfSignCertificatePfx(String x500)
    {
        var dt = DateTime.UtcNow;
        return CreateSelfSignCertificatePfx(x500, dt, dt.AddYears(2), (SecureString?)null);
    }

    /// <summary>建立自签名证书</summary>
    /// <param name="x500"></param>
    /// <param name="startTime"></param>
    /// <param name="endTime"></param>
    /// <returns></returns>
    public static Byte[] CreateSelfSignCertificatePfx(String x500, DateTime startTime, DateTime endTime) => CreateSelfSignCertificatePfx(x500, startTime, endTime, (SecureString?)null);

    /// <summary>建立自签名证书</summary>
    /// <param name="x500"></param>
    /// <param name="startTime"></param>
    /// <param name="endTime"></param>
    /// <param name="insecurePassword"></param>
    /// <returns></returns>
    public static Byte[] CreateSelfSignCertificatePfx(String x500, DateTime startTime, DateTime endTime, String insecurePassword)
    {
        SecureString? password = null;

        try
        {
            if (!String.IsNullOrEmpty(insecurePassword))
            {
                password = new SecureString();
                foreach (var ch in insecurePassword)
                {
                    password.AppendChar(ch);
                }

                password.MakeReadOnly();
            }

            return CreateSelfSignCertificatePfx(x500, startTime, endTime, password);
        }
        finally
        {
            if (password != null) password.Dispose();
        }
    }

    /// <summary>建立自签名证书</summary>
    /// <param name="x500">例如CN=SelfSignCertificate;C=China;OU=NewLife;O=Development Team;E=nnhy@vip.qq.com，其中CN是显示名</param>
    /// <param name="startTime"></param>
    /// <param name="endTime"></param>
    /// <param name="password"></param>
    /// <returns></returns>
    public static Byte[] CreateSelfSignCertificatePfx(String x500, DateTime startTime, DateTime endTime, SecureString? password)
    {
        if (String.IsNullOrEmpty(x500)) x500 = "CN=" + Environment.MachineName;

        //X500DistinguishedNameFlags flag = X500DistinguishedNameFlags.UseUTF8Encoding;
        return CreateSelfSignCertificatePfx(new X500DistinguishedName(x500), startTime, endTime, password);
    }

    /// <summary>建立自签名证书</summary>
    /// <param name="distName"></param>
    /// <param name="startTime"></param>
    /// <param name="endTime"></param>
    /// <param name="password"></param>
    /// <returns></returns>
    public static Byte[] CreateSelfSignCertificatePfx(X500DistinguishedName distName, DateTime startTime, DateTime endTime, SecureString? password)
    {
        var containerName = Guid.NewGuid().ToString();

        var dataHandle = new GCHandle();
        var providerContext = IntPtr.Zero;
        var cryptKey = IntPtr.Zero;
        var certContext = IntPtr.Zero;
        var certStore = IntPtr.Zero;
        var storeCertContext = IntPtr.Zero;
        var passwordPtr = IntPtr.Zero;

        //#if !NETCOREAPP
        //            RuntimeHelpers.PrepareConstrainedRegions();
        //#endif

        try
        {
            Check(NativeMethods.CryptAcquireContextW(
                out providerContext,
                containerName,
                null,
                1, // PROV_RSA_FULL
                8)); // CRYPT_NEWKEYSET

            Check(NativeMethods.CryptGenKey(
                providerContext,
                1, // AT_KEYEXCHANGE
                1, // CRYPT_EXPORTABLE
                out cryptKey));

            var nameData = distName.RawData;

            dataHandle = GCHandle.Alloc(nameData, GCHandleType.Pinned);
            var nameBlob = new CryptoApiBlob(nameData.Length, dataHandle.AddrOfPinnedObject());

            var kpi = new CryptKeyProviderInformation
            {
                ContainerName = containerName,
                ProviderType = 1, // PROV_RSA_FULL
                KeySpec = 1 // AT_KEYEXCHANGE
            };

            var startSystemTime = ToSystemTime(startTime);
            var endSystemTime = ToSystemTime(endTime);
            certContext = NativeMethods.CertCreateSelfSignCertificate(
                providerContext,
                ref nameBlob,
                0,
                ref kpi,
                IntPtr.Zero, // default = SHA1RSA
                ref startSystemTime,
                ref endSystemTime,
                IntPtr.Zero);
            Check(certContext != IntPtr.Zero);
            dataHandle.Free();

            certStore = NativeMethods.CertOpenStore(
                "Memory", // sz_CERT_STORE_PROV_MEMORY
                0,
                IntPtr.Zero,
                0x2000, // CERT_STORE_CREATE_NEW_FLAG
                IntPtr.Zero);
            Check(certStore != IntPtr.Zero);

            Check(NativeMethods.CertAddCertificateContextToStore(
                certStore,
                certContext,
                1, // CERT_STORE_ADD_NEW
                out storeCertContext));

            NativeMethods.CertSetCertificateContextProperty(
                storeCertContext,
                2, // CERT_KEY_PROV_INFO_PROP_ID
                0,
                ref kpi);

            if (password != null)
            {
                passwordPtr = Marshal.SecureStringToCoTaskMemUnicode(password);
            }

            var pfxBlob = new CryptoApiBlob();
            Check(NativeMethods.PFXExportCertStoreEx(
                certStore,
                ref pfxBlob,
                passwordPtr,
                IntPtr.Zero,
                7)); // EXPORT_PRIVATE_KEYS | REPORT_NO_PRIVATE_KEY | REPORT_NOT_ABLE_TO_EXPORT_PRIVATE_KEY

            var pfxData = new Byte[pfxBlob.DataLength];
            dataHandle = GCHandle.Alloc(pfxData, GCHandleType.Pinned);
            pfxBlob.Data = dataHandle.AddrOfPinnedObject();
            Check(NativeMethods.PFXExportCertStoreEx(
                certStore,
                ref pfxBlob,
                passwordPtr,
                IntPtr.Zero,
                7)); // EXPORT_PRIVATE_KEYS | REPORT_NO_PRIVATE_KEY | REPORT_NOT_ABLE_TO_EXPORT_PRIVATE_KEY
            dataHandle.Free();

            return pfxData;
        }
        finally
        {
            if (passwordPtr != IntPtr.Zero) Marshal.ZeroFreeCoTaskMemUnicode(passwordPtr);

            if (dataHandle.IsAllocated) dataHandle.Free();

            if (certContext != IntPtr.Zero) NativeMethods.CertFreeCertificateContext(certContext);

            if (storeCertContext != IntPtr.Zero) NativeMethods.CertFreeCertificateContext(storeCertContext);

            if (certStore != IntPtr.Zero) NativeMethods.CertCloseStore(certStore, 0);

            if (cryptKey != IntPtr.Zero) NativeMethods.CryptDestroyKey(cryptKey);

            if (providerContext != IntPtr.Zero)
            {
                NativeMethods.CryptReleaseContext(providerContext, 0);
                NativeMethods.CryptAcquireContextW(
                    out providerContext,
                    containerName,
                    null,
                    1, // PROV_RSA_FULL
                    0x10); // CRYPT_DELETEKEYSET
            }
        }
    }

    private static SystemTime ToSystemTime(DateTime dateTime)
    {
        var fileTime = dateTime.ToFileTime();
        Check(NativeMethods.FileTimeToSystemTime(ref fileTime, out var systemTime));
        return systemTime;
    }

    private static void Check(Boolean nativeCallSucceeded)
    {
        if (!nativeCallSucceeded)
        {
            var error = Marshal.GetHRForLastWin32Error();
            Marshal.ThrowExceptionForHR(error);
        }
    }

    [StructLayout(LayoutKind.Sequential)]
    private struct SystemTime
    {
        public Int16 Year;
        public Int16 Month;
        public Int16 DayOfWeek;
        public Int16 Day;
        public Int16 Hour;
        public Int16 Minute;
        public Int16 Second;
        public Int16 Milliseconds;
    }

    [StructLayout(LayoutKind.Sequential)]
    private struct CryptoApiBlob
    {
        public Int32 DataLength;
        public IntPtr Data;

        public CryptoApiBlob(Int32 dataLength, IntPtr data)
        {
            DataLength = dataLength;
            Data = data;
        }
    }

    [StructLayout(LayoutKind.Sequential)]
    private struct CryptKeyProviderInformation
    {
        [MarshalAs(UnmanagedType.LPWStr)]
        public String ContainerName;

        [MarshalAs(UnmanagedType.LPWStr)]
        public String ProviderName;

        public Int32 ProviderType;
        public Int32 Flags;
        public Int32 ProviderParameterCount;
        public IntPtr ProviderParameters; // PCRYPT_KEY_PROV_PARAM
        public Int32 KeySpec;
    }

    private static class NativeMethods
    {
        [DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)]
        [return: MarshalAs(UnmanagedType.Bool)]
        public static extern Boolean FileTimeToSystemTime(
            [In] ref Int64 fileTime,
            out SystemTime systemTime);

        [DllImport("AdvApi32.dll", SetLastError = true, ExactSpelling = true)]
        [return: MarshalAs(UnmanagedType.Bool)]
        public static extern Boolean CryptAcquireContextW(
            out IntPtr providerContext,
            [MarshalAs(UnmanagedType.LPWStr)] String container,
            [MarshalAs(UnmanagedType.LPWStr)] String provider,
            Int32 providerType,
            Int32 flags);

        [DllImport("AdvApi32.dll", SetLastError = true, ExactSpelling = true)]
        [return: MarshalAs(UnmanagedType.Bool)]
        public static extern Boolean CryptReleaseContext(
            IntPtr providerContext,
            Int32 flags);

        [DllImport("AdvApi32.dll", SetLastError = true, ExactSpelling = true)]
        [return: MarshalAs(UnmanagedType.Bool)]
        public static extern Boolean CryptGenKey(
            IntPtr providerContext,
            Int32 algorithmId,
            Int32 flags,
            out IntPtr cryptKeyHandle);

        [DllImport("AdvApi32.dll", SetLastError = true, ExactSpelling = true)]
        [return: MarshalAs(UnmanagedType.Bool)]
        public static extern Boolean CryptDestroyKey(
            IntPtr cryptKeyHandle);

        [DllImport("Crypt32.dll", SetLastError = true, ExactSpelling = true)]
        [return: MarshalAs(UnmanagedType.Bool)]
        public static extern Boolean CertStrToNameW(
            Int32 certificateEncodingType,
            IntPtr x500,
            Int32 strType,
            IntPtr reserved,
            [MarshalAs(UnmanagedType.LPArray)][Out] Byte[] encoded,
            ref Int32 encodedLength,
            out IntPtr errorString);

        [DllImport("Crypt32.dll", SetLastError = true, ExactSpelling = true)]
        public static extern IntPtr CertCreateSelfSignCertificate(
            IntPtr providerHandle,
            [In] ref CryptoApiBlob subjectIssuerBlob,
            Int32 flags,
            [In] ref CryptKeyProviderInformation keyProviderInformation,
            IntPtr signatureAlgorithm,
            [In] ref SystemTime startTime,
            [In] ref SystemTime endTime,
            IntPtr extensions);

        [DllImport("Crypt32.dll", SetLastError = true, ExactSpelling = true)]
        [return: MarshalAs(UnmanagedType.Bool)]
        public static extern Boolean CertFreeCertificateContext(
            IntPtr certificateContext);

        [DllImport("Crypt32.dll", SetLastError = true, ExactSpelling = true)]
        public static extern IntPtr CertOpenStore(
            [MarshalAs(UnmanagedType.LPWStr)] String storeProvider,
            Int32 messageAndCertificateEncodingType,
            IntPtr cryptProvHandle,
            Int32 flags,
            IntPtr parameters);

        [DllImport("Crypt32.dll", SetLastError = true, ExactSpelling = true)]
        [return: MarshalAs(UnmanagedType.Bool)]
        public static extern Boolean CertCloseStore(
            IntPtr certificateStoreHandle,
            Int32 flags);

        [DllImport("Crypt32.dll", SetLastError = true, ExactSpelling = true)]
        [return: MarshalAs(UnmanagedType.Bool)]
        public static extern Boolean CertAddCertificateContextToStore(
            IntPtr certificateStoreHandle,
            IntPtr certificateContext,
            Int32 addDisposition,
            out IntPtr storeContextPtr);

        [DllImport("Crypt32.dll", SetLastError = true, ExactSpelling = true)]
        [return: MarshalAs(UnmanagedType.Bool)]
        public static extern Boolean CertSetCertificateContextProperty(
            IntPtr certificateContext,
            Int32 propertyId,
            Int32 flags,
            [In] ref CryptKeyProviderInformation data);

        [DllImport("Crypt32.dll", SetLastError = true, ExactSpelling = true)]
        [return: MarshalAs(UnmanagedType.Bool)]
        public static extern Boolean PFXExportCertStoreEx(
            IntPtr certificateStoreHandle,
            ref CryptoApiBlob pfxBlob,
            IntPtr password,
            IntPtr reserved,
            Int32 flags);
    }
}
